Micron Document
🎖️GitЯра🎖️

Commit baef2a594d02337a0914df2f9e3782ec07d68195


Parents : b3e3804
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-13T11:30:42Z
Committer : GitHub <noreply@github.com>
Date : 2026-08-13T11:30:42Z

test: keep ViewModel coroutines inside the test that started them (#6669)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

Changes
Diff

diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt
index 4c26e5bd94..0e52fb2779 100644
--- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt
+++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt
@@ -31,6 +31,7 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
+import org.meshtastic.core.common.util.safeCatchingAll
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceHardware
@@ -40,6 +41,11 @@ import org.meshtastic.core.repository.Notification
import org.meshtastic.core.repository.NotificationManager
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.firmware_update_available
+import org.meshtastic.core.resources.firmware_update_notification_android
+import org.meshtastic.core.resources.firmware_update_notification_flasher
+import org.meshtastic.core.resources.getString
import org.meshtastic.core.testing.FakeDeviceHardwareRepository
import org.meshtastic.core.testing.FakeFirmwareReleaseRepository
import org.meshtastic.core.testing.FakeNodeRepository
@@ -85,6 +91,7 @@ class ConnectionsViewModelTest {
@BeforeTest
fun setUp() {
+ warmFirmwareNotificationStrings()
Dispatchers.setMain(testDispatcher)
dispatchedNotifications.clear()
notificationsCanBeScheduled = true
@@ -286,4 +293,18 @@ class ConnectionsViewModelTest {
fun `RECONNECTING_PROGRESS_TEXT pins the cross-track literal value`() {
assertEquals("Reconnecting\u2026", ServiceRepository.RECONNECTING_PROGRESS_TEXT)
}
+
+ /**
+ * From CMP 1.12 compose resources load each string once on a library-owned `Dispatchers.Default` scope, which
+ * `advanceUntilIdle` cannot drain, so the notification dispatch lands after the assertions. Pre-loading keeps the
+ * path inside virtual time on any CMP version; must run before `setMain`. Best-effort: a warm-up that cannot load
+ * (skiko's static initializer on the desktop test classpath) must leave the suite as it was, not fail every test.
+ */
+ private fun warmFirmwareNotificationStrings() {
+ safeCatchingAll {
+ getString(Res.string.firmware_update_available)
+ getString(Res.string.firmware_update_notification_android, "", "")
+ getString(Res.string.firmware_update_notification_flasher, "", "")
+ }
+ }
}

diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
index 51289a01f6..137a98edae 100644
--- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
+++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
@@ -16,6 +16,7 @@
*/
package org.meshtastic.feature.settings.radio
+import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import dev.mokkery.MockMode
import dev.mokkery.answering.calls
@@ -31,6 +32,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -123,7 +125,15 @@ class RadioConfigViewModelTest {
private val uiPrefs: UiPrefs = mock(MockMode.autofill)
private val securityKeyBackupStore: SecurityKeyBackupStore = mock(MockMode.autofill)
private val snackbarManager: SnackbarManager = mock(MockMode.autofill)
- private val nodeRestartTracker = NodeRestartTracker(CoroutineScope(SupervisorJob()))
+ private val trackerScope = CoroutineScope(SupervisorJob())
+ private val nodeRestartTracker = NodeRestartTracker(trackerScope)
+
+ /**
+ * A `viewModelScope` is not a child of `runTest`, so work still in flight when a test ends would resume on
+ * `Dispatchers.Main` after [Dispatchers.resetMain] and fail an unrelated later test. Every ViewModel is tracked
+ * here so [tearDown] can cancel it.
+ */
+ private val createdViewModels = mutableListOf<RadioConfigViewModel>()
private lateinit var viewModel: RadioConfigViewModel
@@ -157,6 +167,9 @@ class RadioConfigViewModelTest {
@AfterTest
fun tearDown() {
+ createdViewModels.forEach { it.viewModelScope.cancel() }
+ createdViewModels.clear()
+ trackerScope.cancel()
Dispatchers.resetMain()
}
@@ -186,6 +199,7 @@ class RadioConfigViewModelTest {
lockdownCoordinator = FakeLockdownCoordinator(),
analytics = mock(MockMode.autofill),
)
+ .also { createdViewModels += it }
@Test
fun `setConfig calls useCase`() = runTest {
@@ -951,33 +965,7 @@ class RadioConfigViewModelTest {
fun `destNum from SavedStateHandle resolves destNode`() = runTest {
val node = Node(num = 456, user = User(id = "!456"))
nodeRepository.setNodes(listOf(node))
- viewModel =
- RadioConfigViewModel(
- destNum = 456,
- radioConfigRepository = radioConfigRepository,
- packetRepository = packetRepository,
- serviceRepository = serviceRepository,
- nodeRepository = nodeRepository,
- locationRepository = locationRepository,
- mapConsentPrefs = mapConsentPrefs,
- analyticsPrefs = analyticsPrefs,
- homoglyphEncodingPrefs = homoglyphEncodingPrefs,
- importProfileUseCase = importProfileUseCase,
- exportProfileUseCase = exportProfileUseCase,
- importSecurityConfigUseCase = importSecurityConfigUseCase,
- securityKeyBackupStore = securityKeyBackupStore,
- snackbarManager = snackbarManager,
- nodeRestartTracker = nodeRestartTracker,
- installProfileUseCase = installProfileUseCase,
- radioConfigUseCase = radioConfigUseCase,
- adminActionsUseCase = adminActionsUseCase,
- processRadioResponseUseCase = processRadioResponseUseCase,
- locationService = locationService,
- fileService = fileService,
- mqttManager = mqttManager,
- lockdownCoordinator = FakeLockdownCoordinator(),
- analytics = mock(MockMode.autofill),
- )
+ viewModel = createViewModel(destNum = 456)
assertEquals(456, viewModel.destNode.value?.num)
}

Served by rngit 1.5.0 - Generated in 0.06s